fix(host-chat): drop private flag so the package is publishable#180
Merged
Conversation
…ng service
Wire-compatible with the V2 SSO protocol:
VersionedHandshakeProposal::V2 — emitted by the host via QR carrying
Device { statementAccountId, encryptionPublicKey } and metadata
(HostName / HostVersion / HostIcon / PlatformType / PlatformVersion / Custom)
VersionedHandshakeResponse — answer over Statement Store, ECDH-encrypted
body; inner payload is `EncryptedHandshakeResponseV2 = Pending | Success
| Failed`. Success carries identity_chat_pubkey + identity_sr25519_pubkey
+ 64-byte sr25519 identity_signature.
Pieces:
- scale/handshakeV2.ts — SCALE codecs. V2 is at discriminant 1 (with a
`_v1Reserved: _void` slot to push it past the legacy V1 index 0).
EncryptedHandshakeResponseV2 is a length-dispatched custom codec
(1 byte / 161 bytes / variable str) since the peer's SCALE library
elides the outer enum index for class-wrapped sealed-interface variants.
- v2/topic.ts — pairing topic + channel derivation:
khash(statementAccountId, encryptionPublicKey || "topic"|"channel")
- v2/proposal.ts — encode + build `polkadotapp://pair?handshake=<hex>`.
- v2/envelope.ts — ECDH (P-256) + AES-GCM via @novasamatech/statement-store
createEncryption.
- v2/state.ts — Idle -> Submitted -> Pending(AllowanceAllocation) ->
Success | Failed; forward-only transitions with same-tag idempotence.
- v2/service.ts — orchestrator: subscribes + polls the pairing topic,
SCALE-decodes statements, drives the state machine. Exposes optional
initialProcessedDataHex + onStatementProcessed so callers can
persist byte-level dedupe across reloads (e.g. for proper logout).
Adds rxjs and @polkadot-api/utils to host-papp deps. 60 new tests.
Adds a `## V2 SSO handshake` section covering:
- protocol overview vs V1 (incompat)
- flow diagram (host ↔ peer)
- building the proposal QR via `buildPairingDeeplink`
- driving the handshake via `startPairingV2`, the state machine, and abort
- surviving reloads + proper logout via `initialProcessedDataHex` /
`onStatementProcessed` byte-level dedupe
- topic/channel derivation helpers
- SCALE codec exports table (proposal, response, success, signature payload)
The existing "Authentication and pairing" section is now flagged as the V1
flow and links to the V2 section, so callers can pick the right path.
Per the multi-device chat spec (HackMD Ski9naYdWe), PApp shares the user identity chat keypair with each paired device so the device can decrypt incoming chat traffic addressed to the user identity. Today's HandshakeSuccessV2 wire payload only carries the public half; this commit extends it with a 32-byte raw P-256 private scalar (identityChatPrivateKey) appended after identitySignature. Wire impact: Success payload grows from 161 to 193 bytes. The length- dispatched EncryptedHandshakeResponseV2 codec recognises the new length as Success; older Pending (1 byte) and Failed (variable string) variants keep the same shape. Encryption is unchanged - the outer envelope's ECDH-AES wrap already protects the payload in transit. Mirrored on the responder side (PApp) per the same spec; consumers (paired devices) persist the priv only in OS-keychain-backed secure storage and never forward it.
Initial pass forced HandshakeSuccessV2 to 193 bytes (with the new chat priv field), which broke pairing against PApp builds that haven't yet shipped the multi-device extension - the codec saw 161-byte legacy Success payloads and fell through to the variable-length Failed branch. Now length-dispatched: 161 bytes decodes as legacy Success with identityChatPrivateKey = undefined, 193 bytes decodes as the new extended Success. Encode emits the 193-byte form when a priv key is provided, otherwise falls back to the legacy struct. HandshakeSuccessState.identityChatPrivateKey becomes Uint8Array | undefined so consumers can branch on availability. Send-only V2 paths continue to work without it; inbound chat-request decryption is gated on the field being present. Removes the wire-incompatibility introduced in the previous commit without rolling back the spec-aligned shape; once every PApp build ships the priv key the legacy branch can be retired.
…vate_key The V2 multi-device handshake spec now ships only the user identity chat P-256 private scalar (32 bytes) on the wire; the matching public key is derived locally via P-256 scalar multiplication. Wire format shrinks from 193 to 128 bytes (accountId || identityChatPrivateKey || identitySignature). Legacy 161-byte payloads (encryptionKey || accountId || signature) are still accepted for PApp builds without the multi-device extension. identitySignature now commits to (accountId || derive_pub(identityChatPrivateKey)).
Align the V2 SSO handshake codec with the multi-device spec (https://hackmd.io/@1JCaGppGSUqHtJilikYaKw/Ski9naYdWe). Success body shape: identityAccountId(32) || rootAccountId(32) || identityChatPrivateKey(32) || deviceEncPubKey(65) = 161 bytes (spec v0.2.1) Also accept the 129-byte v0.2 variant (no rootAccountId) emitted by Android's `feature/location-for-handshake`. Surface `rootAccountId: null` in that case — chat does not need it; product-account derivation degrades gracefully. Removed: - `identitySignature` field (multi-device authorisation moves to the user-identity-signed roster events DeviceAdded/DeviceRemoved) - `IDENTITY_SIGNATURE_PAYLOAD_BYTES` export - `HandshakeSuccessV2WithChatPriv` (was the experimental 128-byte shape) Added: - `HandshakeSuccessV2Value` exported type - `decodeEncryptedHandshakeResponseV2` — explicit length-dispatched decoder for the inner plaintext - `deriveIdentityChatPublicKey` — P-256 scalar mult helper - `EncryptedHandshakeResponseV2` now built with native scale-ts `Enum` on the inner discriminant. The peer SCALE library does NOT elide the variant index, so `Pending(AllowanceAllocation)` arrives as `0x00 0x00` and was being misclassified as `Failed("")` by the prior length-only dispatch. `HandshakeSuccessState` now exposes identityAccountId, rootAccountId, identityChatPrivateKey, identityChatPublicKey (derived locally) and deviceEncPubKey. Pairing service decodes via the new `decodeEncryptedHandshakeResponseV2` and logs failure reasons with the raw inner bytes for diagnosis.
V2 multi-device runtime metadata exposes Resources.Consumers fields in camelCase, which crashed `raw.stmt_store_slots.map(...)` in the host-papp identity adapter. Read each field with snake/camel fallback and treat slots as optional. Same defensiveness applied to host-chat's getConsumerInfo. The .papi descriptor types only model snake_case, so widen via `Record<string, unknown>` at the read site.
Adopt the multi-device chat content shape per the chat spec v0.1 (https://hackmd.io/@1JCaGppGSUqHtJilikYaKw/Ski9naYdWe). New MessageContent variants: - chatAccepted (14) — legacy single-device accept payload changed from `_void` to `ChatAcceptedContent { messageId: String }` for iOS V1 backward decode. - _reserved16 (16) — reserved (Android `coinagePayment`, unused on desktop) - deviceAdded (17) — `{ statementAccountId, encryptionPublicKey }` - deviceRemoved (18) — `{ statementAccountId }` - _reserved19 (19) — placeholder so deviceChatAccepted lands at the spec'd index 20. - deviceChatAccepted (20) — `{ requestId, device: DeviceInfo }`. Sent via identity-level session SessionId(B, A) encrypted with K(A,B); identity-level encryption lets all of A's devices decrypt without a per-device envelope. DeviceAdded/Removed use length-prefixed `Bytes()` instead of fixed-size codecs because substrate-sdk-android emits the `AccountId` / `EncodedPublicKey` wrapper types without `@FixedLength`, falling through to length-prefixed `Vec<u8>` on the wire. DeviceInfoContent (used by deviceChatAccepted) stays fixed-size — Android's `DeviceInfoScale` declares `@FixedLength` explicitly.
`npm ci` in CI requires package-lock.json to be in sync with package.json. The V2 SSO commits added @polkadot-api/utils, rxjs, and verifiablejs to host-papp but the lockfile wasn't refreshed when the branch was first pushed.
`createAuth` / `pappAdapter.sso` is now V2-driven end-to-end with the same
`pairingStatus` / `authenticate()` / `abortAuthentication()` surface as
before — the V2 wire format, ECDH envelope, pairing service, state
machine, topic derivation, and peer-signer capture all run inside the
SDK. The V1 handshake codec and ephemeral-keypair logic are gone;
callers must inject a persistent `DeviceIdentityForPairing` via a thunk
on `createPappAdapter`.
`createPappAdapter` reshape:
- drops `metadata: string` (the V1 metadata URL) — host name / icon /
platform now ride inside `hostMetadata` (which mirrors V2's
`HandshakeMetadata`)
- requires `deviceIdentity: () => Promise<DeviceIdentityForPairing>`
- new `onAuthSuccess`, `initialProcessedDataHex`,
`onPairingStatementProcessed` callbacks for consumer-side
persistence and reload-survival dedupe
`AuthSuccess.peerStatementAccountId` is lifted off
`statement.proof.value.signer` during pairing so device-sync can seed
PApp without a follow-up chain query. Threaded through
`HandshakeSuccessState` and `fromInnerResponse`.
Public surface trimmed to `createAuth` and the types you need to use
it. `startPairingV2`, the state-machine helpers (`idle` / `submitted` /
`advance` / `fromInnerResponse` / `isTerminal` /
`canSubmitV2Statements`), topic / envelope / proposal helpers
(`computePairingTopic` / `computePairingChannel` /
`buildPairingDeeplink` / `encodeProposal` / `decryptResponseEnvelope` /
`deriveIdentityChatPublicKey`), every `Handshake*` /
`*HandshakeResponse*` / `VersionedHandshake*` / `MetadataEntry` /
`MetadataKey` / `Device` SCALE codec, and the `Handshake*State` /
`HandshakeMetadata` / `HandshakeProposalDevice` /
`HandshakeResponseEnvelope` / `Pairing` / `StartPairingDeps` types are
all internal now. `PairingStatus.finished` no longer carries `session`
— read the resolved `AuthSuccess` off `authenticate()` instead.
`host-papp-react-ui`:
- `useAuthStatus` returns `{ status, isSignedIn }` (was `signedInUser`,
which depended on the V1 session shape)
- `Flow.stories.tsx` updated for the new `createPappAdapter` params
523/523 tests pass. `auth.spec.ts` rewritten to drive the V2-backed
`createAuth` end-to-end (success, persistOnSuccess hook, Failed inner
response, abort, debug emits) using a real SCALE-encoded
HandshakeSuccessV2 envelope.
The previous draft mixed two reference frames: some items described the diff vs. 0.7.8 (correct for a changelog), others described the diff vs. an earlier state of this branch where V2 helpers were briefly public (internal to PR #178 development, irrelevant to consumers). The latter read as "no longer something callers need to wire up" / "public surface trimmed" / "fix: EncryptedHandshakeResponseV2 misclassification" — none of those make sense for a consumer upgrading from 0.7.8, where V2 SSO didn't exist publicly at all. Reframed: - Lead Features bullet now states V2 SSO as a fresh capability behind the existing createAuth surface, with the V2 codecs/service/state- machine flagged as SDK internals (not as "no longer public"). - Dropped the "public surface trimmed" Breaking Change — those names were never in 0.7.8. - Dropped the EncryptedHandshakeResponseV2 fix bullet — that was a bug introduced and fixed inside this PR, not visible to 0.7.8 users. - Merged the legacy-payload-removal bullet into the main V1-removed bullet to avoid double-flagging the same break.
…K; restore V1-shape auth surface
The 0.7.x → 0.8.0 migration is now two field renames (`metadata` →
inline `hostMetadata`, `osType`/`osVersion` → `platformType`/
`platformVersion`). Everything else about the auth surface — the
`pairingStatus` / `authenticate()` / `abortAuthentication()` triad,
`PairingStatus.finished.session`, `authenticate()` resolving to
`StoredUserSession | null` — is what it was.
What moved into the SDK:
- New internal `deviceIdentityStore` (encrypted via the same
gcm/blake2b pattern as `UserSecretRepository`). `createPappAdapter`
now defaults `deviceIdentity` to a `StorageAdapter`-backed factory
that generates and persists a fresh identity on first run. Override
with the new optional `deviceIdentity` param if you need a
different backend (Electron Keychain, native secure storage).
- Pairing-topic statement dedupe (`initialProcessedDataHex` /
`onPairingStatementProcessed` in the previous draft) is now
fully internal; the consumer-facing surface drops both.
- On Success, `createAuth` builds a V2-shaped `StoredUserSession` and
writes it + the per-session secrets to `ssoSessionRepository` and
`userSecretRepository` itself. `authenticate()` returns the
persisted `StoredUserSession`. The optional `onAuthSuccess` hook
fires after persistence with `{ session, identityChatPrivateKey }`
so consumers can fan the user-identity bits out to their own
stores (e.g. polkadot-desktop's `deviceIdentityRepository`).
Schema changes:
- `StoredUserSession` gains `identityAccountId?` and
`identityChatPublicKey?` as trailing `Option` fields; the peer
device statement account is exposed via `remoteAccount.accountId`.
`from` decoder wraps in try/catch and returns `[]` on V1-blob
decode failure, so the SDK silently wipes 0.7.x SsoSessions on
first read instead of crashing.
- `UserSecretRepository` gains `identityChatPrivateKey: Bytes(32)`
as a trailing required field. `decode` wraps in try/catch and
returns `null` on V1-blob decode failure.
- `DeviceIdentityForPairing` adds a required `statementAccountSecret`
(64-byte expanded sr25519 secret) so the V1 sessionManager prover
can sign session statements with the device's stable identity.
Public surface stays compact: `createPappAdapter`, `PappAdapter`,
`AuthComponent`, `HostMetadata`, `OnAuthSuccess`, `PairingStatus`,
`DeviceIdentityForPairing`, `StoredUserSession`, `UserSession`,
`Identity`, signing request/response types, `RingVrf*`,
`SS_*_ENDPOINTS`. `AuthSuccess` is gone — `StoredUserSession` carries
the same fields.
`host-papp-react-ui`:
- `useAuthStatus` restores `signedInUser` (reads
`pairingStatus.finished.session`)
- `Flow.stories.tsx` drops the `deviceIdentity` stub — the SDK now
defaults it.
524/524 tests pass. New tests cover internal persistence, the
`onAuthSuccess` hook, and the default-deviceIdentity fallback.
V2 People chain endpoints — needed by desktop env constants that have been falling back to paseo-next while the V2 entry was missing.
host-chat was marked private:"true" and silently skipped by the release pipeline — the 0.8.0-0 release only shipped host-api/host-papp/etc. Downstream desktop has to pin host-chat to a file: workspace ref, which breaks CI consumers that don't have the sibling SDK checkout. Removing the flag lets the next release publish it alongside the rest of the V2 SDK surface.
kalininilya
changed the base branch from
main
to
feat/multi-device-sdk-v2-rebased
May 22, 2026 13:13
The existing README was an unedited copy of host-container's docs (wrong title, none of the host-chat surface mentioned). Rewrite to cover what the package actually does: createAccountService, network selection, search / getConsumerInfo semantics, and the codec subpath exports.
johnthecat
changed the base branch from
feat/multi-device-sdk-v2-rebased
to
release/0.8
May 22, 2026 13:28
# Conflicts: # CHANGELOG.md # package-lock.json # packages/host-chat/package.json # packages/host-papp/package.json # packages/host-papp/src/sso/auth/v2/proposal.ts
johnthecat
approved these changes
May 22, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
"private": "true"frompackages/host-chat/package.jsonso the release pipeline ships it alongside the rest of the V2 SDK surface.Background
The 0.8.0-0 release published
host-api,host-papp,scale, etc. but skippedhost-chatbecause of theprivateflag. Downstream desktop pins it tofile:../triangle-js-sdks/packages/host-chat, which only works for local dev and breaks CI consumers that don't have the sibling SDK checkout.Test plan
file:ref inpolkadot-desktop/package.jsonand reinstall